<?php
class GifFrameExtractor {
	// Properties
	// ===================================================================================

	/**
	 * @var resource
	 */
	private $gif;

	/**
	 * @var array
	 */
	private $frames;

	/**
	 * @var array
	 */
	private $frameDurations;

	/**
	 * @var array
	 */
	private $frameImages;

	/**
	 * @var array
	 */
	private $framePositions;

	/**
	 * @var array
	 */
	private $frameDimensions;

	/**
	 * @var integer
	 *
	 * (old: $this->index)
	 */
	private $frameNumber;

	/**
	 * @var array
	 *
	 * (old: $this->imagedata)
	 */
	private $frameSources;

	/**
	 * @var array
	 *
	 * (old: $this->fileHeader)
	 */
	private $fileHeader;

	/**
	 * @var integer The reader pointer in the file source
	 *
	 * (old: $this->pointer)
	 */
	private $pointer;

	/**
	 * @var integer
	 */
	private $gifMaxWidth;

	/**
	 * @var integer
	 */
	private $gifMaxHeight;

	/**
	 * @var integer
	 */
	private $totalDuration;

	/**
	 * @var integer
	 */
	private $handle;

	/**
	 * @var array
	 *
	 * (old: globaldata)
	 */
	private $globaldata;

	/**
	 * @var array
	 *
	 * (old: orgvars)
	 */
	private $orgvars;

	// Methods
	// ===================================================================================

	/**
	 * Extract frames of a GIF
	 *
	 * @param string $filename GIF filename path
	 * @param boolean $originalFrames Get original frames (with transparent background)
	 *
	 * @return array
	 */
	public function extract($filename, $originalFrames = false) {
		if (!self::isAnimatedGif($filename)) {
			throw new \Exception('The GIF image you are trying to explode is not animated !');
		}

		$this->reset();
		$this->parseFramesInfo($filename);
		$prevImg = null;

		for ($i = 0; $i < count($this->frameSources); $i++) {

			$this->frames[$i] = array();
			$this->frameDurations[$i] = $this->frames[$i]['duration'] = $this->frameSources[$i]['delay_time'];

			$img = imagecreatefromstring($this->fileHeader["gifheader"] . $this->frameSources[$i]["graphicsextension"] . $this->frameSources[$i]["imagedata"] . chr(0x3b));

			if (!$originalFrames) {

				if ($i > 0) {

					$prevImg = $this->frames[$i - 1]['image'];

				} else {

					$prevImg = $img;
				}

				$sprite = imagecreate($this->gifMaxWidth, $this->gifMaxHeight);
				imagesavealpha($sprite, true);

				$transparent = imagecolortransparent($prevImg);

				if ($transparent > -1 && imagecolorstotal($prevImg) > $transparent) {

					$actualTrans = imagecolorsforindex($prevImg, $transparent);
					imagecolortransparent($sprite, imagecolorallocate($sprite, $actualTrans['red'], $actualTrans['green'], $actualTrans['blue']));
				}

				if ((int) $this->frameSources[$i]['disposal_method'] == 1 && $i > 0) {

					imagecopy($sprite, $prevImg, 0, 0, 0, 0, $this->gifMaxWidth, $this->gifMaxHeight);
				}

				imagecopyresampled($sprite, $img, $this->frameSources[$i]["offset_left"], $this->frameSources[$i]["offset_top"], 0, 0, $this->gifMaxWidth, $this->gifMaxHeight, $this->gifMaxWidth, $this->gifMaxHeight);
				$img = $sprite;
			}

			$this->frameImages[$i] = $this->frames[$i]['image'] = $img;
		}

		return $this->frames;
	}

	/**
	 * Check if a GIF file at a path is animated or not
	 *
	 * @param string $filename GIF path
	 */
	public static function isAnimatedGif($filename) {
		if (!($fh = @fopen($filename, 'rb'))) {
			return false;
		}

		$count = 0;
		while (!feof($fh) && $count < 2) {

			$chunk = fread($fh, 1024 * 100); //read 100kb at a time
			$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
		}

		fclose($fh);
		return $count > 1;
	}

	// Internals
	// ===================================================================================

	/**
	 * Parse the frame informations contained in the GIF file
	 *
	 * @param string $filename GIF filename path
	 */
	private function parseFramesInfo($filename) {
		$this->openFile($filename);
		$this->parseGifHeader();
		$this->parseGraphicsExtension(0);
		$this->getApplicationData();
		$this->getApplicationData();
		$this->getFrameString(0);
		$this->parseGraphicsExtension(1);
		$this->getCommentData();
		$this->getApplicationData();
		$this->getFrameString(1);

		while (!$this->checkByte(0x3b) && !$this->checkEOF()) {

			$this->getCommentData(1);
			$this->parseGraphicsExtension(2);
			$this->getFrameString(2);
			$this->getApplicationData();
		}
	}

	/**
	 * Parse the gif header (old: get_gif_header)
	 */
	private function parseGifHeader() {
		$this->pointerForward(10);

		if ($this->readBits(($mybyte = $this->readByteInt()), 0, 1) == 1) {

			$this->pointerForward(2);
			$this->pointerForward(pow(2, $this->readBits($mybyte, 5, 3) + 1) * 3);

		} else {

			$this->pointerForward(2);
		}
		$this->fileHeader["gifheader"] = $this->dataPart(0, $this->pointer);

		// Decoding
		$this->orgvars["gifheader"] = $this->fileHeader["gifheader"];
		$this->orgvars["background_color"] = $this->orgvars["gifheader"][11];
	}

	/**
	 * Parse the application data of the frames (old: get_application_data)
	 */
	private function getApplicationData() {
		$startdata = $this->readByte(2);

		if ($startdata == chr(0x21) . chr(0xff)) {

			$start = $this->pointer - 2;
			$this->pointerForward($this->readByteInt());
			$this->readDataStream($this->readByteInt());
			$this->fileHeader["applicationdata"] = $this->dataPart($start, $this->pointer - $start);

		} else {

			$this->pointerRewind(2);
		}
	}

	/**
	 * Parse the comment data of the frames (old: get_comment_data)
	 */
	private function getCommentData() {
		$startdata = $this->readByte(2);

		if ($startdata == chr(0x21) . chr(0xfe)) {

			$start = $this->pointer - 2;
			$this->readDataStream($this->readByteInt());
			$this->fileHeader["commentdata"] = $this->dataPart($start, $this->pointer - $start);

		} else {

			$this->pointerRewind(2);
		}
	}

	/**
	 * Parse the graphic extension of the frames (old: get_graphics_extension)
	 *
	 * @param integer $type
	 */
	private function parseGraphicsExtension($type) {
		$startdata = $this->readByte(2);

		if ($startdata == chr(0x21) . chr(0xf9)) {

			$start = $this->pointer - 2;
			$this->pointerForward($this->readByteInt());
			$this->pointerForward(1);

			if ($type == 2) {

				$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->dataPart($start, $this->pointer - $start);

			} elseif ($type == 1) {

				$this->orgvars["hasgx_type_1"] = 1;
				$this->globaldata["graphicsextension"] = $this->dataPart($start, $this->pointer - $start);

			} elseif ($type == 0) {

				$this->orgvars["hasgx_type_0"] = 1;
				$this->globaldata["graphicsextension_0"] = $this->dataPart($start, $this->pointer - $start);
			}

		} else {

			$this->pointerRewind(2);
		}
	}

	/**
	 * Get the full frame string block (old: get_image_block)
	 *
	 * @param integer $type
	 */
	private function getFrameString($type) {
		if ($this->checkByte(0x2c)) {

			$start = $this->pointer;
			$this->pointerForward(9);

			if ($this->readBits(($mybyte = $this->readByteInt()), 0, 1) == 1) {

				$this->pointerForward(pow(2, $this->readBits($mybyte, 5, 3) + 1) * 3);
			}

			$this->pointerForward(1);
			$this->readDataStream($this->readByteInt());
			$this->frameSources[$this->frameNumber]["imagedata"] = $this->dataPart($start, $this->pointer - $start);
			if ($type == 0) {

				$this->orgvars["hasgx_type_0"] = 0;

				if (isset($this->globaldata["graphicsextension_0"])) {

					$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension_0"];

				} else {

					$this->frameSources[$this->frameNumber]["graphicsextension"] = null;
				}

				unset($this->globaldata["graphicsextension_0"]);

			} elseif ($type == 1) {

				if (isset($this->orgvars["hasgx_type_1"]) && $this->orgvars["hasgx_type_1"] == 1) {

					$this->orgvars["hasgx_type_1"] = 0;
					$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension"];
					unset($this->globaldata["graphicsextension"]);

				} else {

					$this->orgvars["hasgx_type_0"] = 0;
					$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension_0"];
					unset($this->globaldata["graphicsextension_0"]);
				}
			}
			$this->parseFrameData();
			$this->frameNumber++;
		}
	}

	/**
	 * Parse frame data string into an array (old: parse_image_data)
	 */
	private function parseFrameData() {
		$this->frameSources[$this->frameNumber]["disposal_method"] = $this->getImageDataBit("ext", 3, 3, 3);
		$this->frameSources[$this->frameNumber]["user_input_flag"] = $this->getImageDataBit("ext", 3, 6, 1);
		$this->frameSources[$this->frameNumber]["transparent_color_flag"] = $this->getImageDataBit("ext", 3, 7, 1);
		$this->frameSources[$this->frameNumber]["delay_time"] = $this->dualByteVal($this->getImageDataByte("ext", 4, 2));
		$this->totalDuration += (int) $this->frameSources[$this->frameNumber]["delay_time"];
		$this->frameSources[$this->frameNumber]["transparent_color_index"] = ord($this->getImageDataByte("ext", 6, 1));
		$this->frameSources[$this->frameNumber]["offset_left"] = $this->dualByteVal($this->getImageDataByte("dat", 1, 2));
		$this->frameSources[$this->frameNumber]["offset_top"] = $this->dualByteVal($this->getImageDataByte("dat", 3, 2));
		$this->frameSources[$this->frameNumber]["width"] = $this->dualByteVal($this->getImageDataByte("dat", 5, 2));
		$this->frameSources[$this->frameNumber]["height"] = $this->dualByteVal($this->getImageDataByte("dat", 7, 2));
		$this->frameSources[$this->frameNumber]["local_color_table_flag"] = $this->getImageDataBit("dat", 9, 0, 1);
		$this->frameSources[$this->frameNumber]["interlace_flag"] = $this->getImageDataBit("dat", 9, 1, 1);
		$this->frameSources[$this->frameNumber]["sort_flag"] = $this->getImageDataBit("dat", 9, 2, 1);
		$this->frameSources[$this->frameNumber]["color_table_size"] = pow(2, $this->getImageDataBit("dat", 9, 5, 3) + 1) * 3;
		$this->frameSources[$this->frameNumber]["color_table"] = substr($this->frameSources[$this->frameNumber]["imagedata"], 10, $this->frameSources[$this->frameNumber]["color_table_size"]);
		$this->frameSources[$this->frameNumber]["lzw_code_size"] = ord($this->getImageDataByte("dat", 10, 1));

		$this->framePositions[$this->frameNumber] = array(
			'x' => $this->frameSources[$this->frameNumber]["offset_left"],
			'y' => $this->frameSources[$this->frameNumber]["offset_top"],
		);

		$this->frameDimensions[$this->frameNumber] = array(
			'width' => $this->frameSources[$this->frameNumber]["width"],
			'height' => $this->frameSources[$this->frameNumber]["height"],
		);

		// Decoding
		$this->orgvars[$this->frameNumber]["transparent_color_flag"] = $this->frameSources[$this->frameNumber]["transparent_color_flag"];
		$this->orgvars[$this->frameNumber]["transparent_color_index"] = $this->frameSources[$this->frameNumber]["transparent_color_index"];
		$this->orgvars[$this->frameNumber]["delay_time"] = $this->frameSources[$this->frameNumber]["delay_time"];
		$this->orgvars[$this->frameNumber]["disposal_method"] = $this->frameSources[$this->frameNumber]["disposal_method"];
		$this->orgvars[$this->frameNumber]["offset_left"] = $this->frameSources[$this->frameNumber]["offset_left"];
		$this->orgvars[$this->frameNumber]["offset_top"] = $this->frameSources[$this->frameNumber]["offset_top"];

		// Updating the max width
		if ($this->gifMaxWidth < $this->frameSources[$this->frameNumber]["width"]) {

			$this->gifMaxWidth = $this->frameSources[$this->frameNumber]["width"];
		}

		// Updating the max height
		if ($this->gifMaxHeight < $this->frameSources[$this->frameNumber]["height"]) {

			$this->gifMaxHeight = $this->frameSources[$this->frameNumber]["height"];
		}
	}

	/**
	 * Get the image data byte (old: get_imagedata_byte)
	 *
	 * @param string $type
	 * @param integer $start
	 * @param integer $length
	 *
	 * @return string
	 */
	private function getImageDataByte($type, $start, $length) {
		if ($type == "ext") {

			return substr($this->frameSources[$this->frameNumber]["graphicsextension"], $start, $length);
		}

		// "dat"
		return substr($this->frameSources[$this->frameNumber]["imagedata"], $start, $length);
	}

	/**
	 * Get the image data bit (old: get_imagedata_bit)
	 *
	 * @param string $type
	 * @param integer $byteIndex
	 * @param integer $bitStart
	 * @param integer $bitLength
	 *
	 * @return number
	 */
	private function getImageDataBit($type, $byteIndex, $bitStart, $bitLength) {
		if ($type == "ext") {

			return $this->readBits(ord(substr($this->frameSources[$this->frameNumber]["graphicsextension"], $byteIndex, 1)), $bitStart, $bitLength);
		}

		// "dat"
		return $this->readBits(ord(substr($this->frameSources[$this->frameNumber]["imagedata"], $byteIndex, 1)), $bitStart, $bitLength);
	}

	/**
	 * Return the value of 2 ASCII chars (old: dualbyteval)
	 *
	 * @param string $s
	 *
	 * @return integer
	 */
	private function dualByteVal($s) {
		$i = ord($s[1]) * 256 + ord($s[0]);

		return $i;
	}

	/**
	 * Read the data stream (old: read_data_stream)
	 *
	 * @param integer $firstLength
	 */
	private function readDataStream($firstLength) {
		$this->pointerForward($firstLength);
		$length = $this->readByteInt();

		if ($length != 0) {

			while ($length != 0) {

				$this->pointerForward($length);
				$length = $this->readByteInt();
			}
		}
	}

	/**
	 * Open the gif file (old: loadfile)
	 *
	 * @param string $filename
	 */
	private function openFile($filename) {
		$this->handle = fopen($filename, "rb");
		$this->pointer = 0;

		$imageSize = getimagesize($filename);
		$this->gifWidth = $imageSize[0];
		$this->gifHeight = $imageSize[1];
	}

	/**
	 * Close the read gif file (old: closefile)
	 */
	private function closeFile() {
		fclose($this->handle);
		$this->handle = 0;
	}

	/**
	 * Read the file from the beginning to $byteCount in binary (old: readbyte)
	 *
	 * @param integer $byteCount
	 *
	 * @return string
	 */
	private function readByte($byteCount) {
		$data = fread($this->handle, $byteCount);
		$this->pointer += $byteCount;

		return $data;
	}

	/**
	 * Read a byte and return ASCII value (old: readbyte_int)
	 *
	 * @return integer
	 */
	private function readByteInt() {
		$data = fread($this->handle, 1);
		$this->pointer++;

		return ord($data);
	}

	/**
	 * Convert a $byte to decimal (old: readbits)
	 *
	 * @param string $byte
	 * @param integer $start
	 * @param integer $length
	 *
	 * @return number
	 */
	private function readBits($byte, $start, $length) {
		$bin = str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
		$data = substr($bin, $start, $length);

		return bindec($data);
	}

	/**
	 * Rewind the file pointer reader (old: p_rewind)
	 *
	 * @param integer $length
	 */
	private function pointerRewind($length) {
		$this->pointer -= $length;
		fseek($this->handle, $this->pointer);
	}

	/**
	 * Forward the file pointer reader (old: p_forward)
	 *
	 * @param integer $length
	 */
	private function pointerForward($length) {
		$this->pointer += $length;
		fseek($this->handle, $this->pointer);
	}

	/**
	 * Get a section of the data from $start to $start + $length (old: datapart)
	 *
	 * @param integer $start
	 * @param integer $length
	 *
	 * @return string
	 */
	private function dataPart($start, $length) {
		fseek($this->handle, $start);
		$data = fread($this->handle, $length);
		fseek($this->handle, $this->pointer);

		return $data;
	}

	/**
	 * Check if a character if a byte (old: checkbyte)
	 *
	 * @param integer $byte
	 *
	 * @return boolean
	 */
	private function checkByte($byte) {
		if (fgetc($this->handle) == chr($byte)) {

			fseek($this->handle, $this->pointer);
			return true;
		}

		fseek($this->handle, $this->pointer);
		return false;
	}

	/**
	 * Check the end of the file (old: checkEOF)
	 *
	 * @return boolean
	 */
	private function checkEOF() {
		if (fgetc($this->handle) === false) {

			return true;
		}

		fseek($this->handle, $this->pointer);

		return false;
	}

	/**
	 * Reset and clear this current object
	 */
	private function reset() {
		$this->gif = null;
		$this->totalDuration = $this->gifMaxHeight = $this->gifMaxWidth = $this->handle = $this->pointer = $this->frameNumber = 0;
		$this->frameDimensions = $this->framePositions = $this->frameImages = $this->frameDurations = $this->globaldata = $this->orgvars = $this->frames = $this->fileHeader = $this->frameSources = array();
	}

	// Getter / Setter
	// ===================================================================================

	/**
	 * Get the total of all added frame duration
	 *
	 * @return integer
	 */
	public function getTotalDuration() {
		return $this->totalDuration;
	}

	/**
	 * Get the number of extracted frames
	 *
	 * @return integer
	 */
	public function getFrameNumber() {
		return $this->frameNumber;
	}

	/**
	 * Get the extracted frames (images and durations)
	 *
	 * @return array
	 */
	public function getFrames() {
		return $this->frames;
	}

	/**
	 * Get the extracted frame positions
	 *
	 * @return array
	 */
	public function getFramePositions() {
		return $this->framePositions;
	}

	/**
	 * Get the extracted frame dimensions
	 *
	 * @return array
	 */
	public function getFrameDimensions() {
		return $this->frameDimensions;
	}

	/**
	 * Get the extracted frame images
	 *
	 * @return array
	 */
	public function getFrameImages() {
		return $this->frameImages;
	}

	/**
	 * Get the extracted frame durations
	 *
	 * @return array
	 */
	public function getFrameDurations() {
		return $this->frameDurations;
	}
}